package in.rob.client.page.base; import in.lib.Constants; import in.lib.Debug; import in.lib.adapter.PostAdapter; import in.lib.adapter.base.RobinAdapter; import in.lib.adapter.base.RobinAdapter.OnPagerListener; import in.lib.annotation.InjectView; import in.lib.helper.ResponseHelper; import in.lib.loader.base.Loader; import in.lib.manager.CacheManager; import in.lib.manager.SettingsManager; import in.lib.thread.FragmentRunnable; import in.lib.utils.Dimension; import in.lib.utils.Views; import in.lib.view.HeadedListView; import in.lib.view.HeadedListView.OnScrollListener; import in.lib.writer.CacheWriter.WriterListener; import in.model.Post; import in.model.Stream; import in.model.base.Message; import in.model.base.NetObject; import in.rob.client.MainApplication; import in.rob.client.R; import in.rob.client.base.RobinListFragment; import in.rob.client.base.RobinSlidingActivity; import java.util.List; import lombok.Getter; import lombok.Setter; import net.callumtaylor.swipetorefresh.helper.RefreshHelper; import net.callumtaylor.swipetorefresh.helper.RefreshHelper.OnRefreshListener; import net.callumtaylor.swipetorefresh.view.RefreshableScrollView; import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.AnimationSet; import android.view.animation.LinearInterpolator; import android.view.animation.TranslateAnimation; import android.widget.AbsListView; import android.widget.AdapterView.OnItemClickListener; import android.widget.TextView; public abstract class StreamFragment extends RobinListFragment implements OnScrollListener, WriterListener, OnRefreshListener, OnItemClickListener { @Getter private RobinAdapter adapter; // views @Getter private View paddingView; @Getter private View loadMoreView; @Getter @InjectView(android.R.id.empty) public RefreshableScrollView emptyListView; @Getter @InjectView(R.id.progress_loader) public View progressLoader; @Getter @InjectView(R.id.new_posts_ticker) public TextView postTicker; @Getter @Setter private boolean allowPagination = true; @Getter private boolean loading = false; private boolean mForceRefresh = false; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.timeline_view, container, false); Views.inject(this, v); return v; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); refreshHelper = RefreshHelper.wrapRefreshable(view, this); if (isLoading() && ((RobinSlidingActivity)getActivity()).getAdapter().getCurrentFragment() == this) { refreshHelper.setRefreshing(true); refreshHelper.showHelper(); } addDefaultFooters(); getHeadedListView().setOnItemClickListener(this); getHeadedListView().addOnScrollListener(this); checkPendingThenExecute(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { mForceRefresh = getArguments().getBoolean(Constants.EXTRA_FORCE_REFRESH, false); getArguments().remove(Constants.EXTRA_FORCE_REFRESH); } retrieveArguments(getArguments()); initData(); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setupAdapters(); attachHandlers(); if (getAdapter() != null) { getAdapter().setListView(getHeadedListView()); } checkPendingThenExecute(); } @Override public void onDestroyView() { Views.reset(this); setListAdapter(null); super.onDestroyView(); } @Override public void onDestroy() { super.onDestroy(); detachHandlers(); } public HeadedListView getHeadedListView() { return ((HeadedListView)getListView()); } /** * Called just after retrieveArguments in onActivityCreated. This * is where the cache loader is called and executed. If no cache is * loaded, or not found {@link beginLoadFromApi()} is called. */ public void initData() { runOnUiThread(new Runnable() { @Override public void run() { new CacheLoader(getCacheFileName()).execute(); } }); } public void setTicker(int count) { if (count > 0) { AnimationSet anim = new AnimationSet(false); TranslateAnimation fwdAnim = new TranslateAnimation ( Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, -1f, Animation.RELATIVE_TO_SELF, 0f ); anim.setInterpolator(new LinearInterpolator()); anim.setDuration(600); TranslateAnimation revAnim = new TranslateAnimation ( Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, -1f ); revAnim.setInterpolator(new LinearInterpolator()); revAnim.setDuration(600); revAnim.setStartOffset(2500); revAnim.setAnimationListener(new AnimationListener() { @Override public void onAnimationStart(Animation animation){} @Override public void onAnimationRepeat(Animation animation){} @Override public void onAnimationEnd(Animation animation) { postTicker.setVisibility(View.GONE); } }); anim.addAnimation(fwdAnim); anim.addAnimation(revAnim); postTicker.setVisibility(View.VISIBLE); postTicker.setText(String.valueOf(count) + " new posts"); postTicker.startAnimation(anim); } } public void setLoading(boolean loading) { this.loading = loading; if (loading) { showProgressLoader(); } else { hideProgressLoader(); } } /** * Gets the position data for the current view position in a list of new * items from a stream * * @param items * The new list of items (to calculate the current view's new * position when this list is added to the adapter) * @return An array of integer, {pos, y pos} */ public int[] getFirstViewPosition(List<? extends NetObject> items) { int pos = getListView().getFirstVisiblePosition(); if (pos < 0) { return new int[]{0, 0, 0}; } String currentPostId = getCurrentItemId(pos); View v = getListView().getChildAt(0); int top = (v == null) ? 0 : v.getTop(); boolean brokenStream = true; if (items != null) { int count = items.size(); for (int index = 0; index < count; index++) { NetObject p = items.get(index); if (p.getId().equals(currentPostId)) { pos = index; } if (getAdapter().getItemById(p.getId()) != null) { brokenStream = false; } } } return new int[]{pos, top, brokenStream ? 1 : 0}; } public int[] getLastViewPosition(List<? extends NetObject> items) { int pos = getListView().getLastVisiblePosition(); if (pos < 0) { return new int[]{0, 0, 0}; } String currentPostId = getCurrentItemId(pos); View v = getListView().getChildAt(Math.max(0, getListView().getChildCount() - 1)); int top = (v == null) ? 0 : v.getTop(); boolean brokenStream = true; if (items != null) { int count = items.size(); for (int index = 0; index < count; index++) { NetObject p = items.get(index); if (p.getId().equals(currentPostId)) { pos = index; } if (getAdapter().getItemById(p.getId()) != null) { brokenStream = false; } } } return new int[]{pos, top, brokenStream ? 1 : 0}; } /** * Finds the first item in the list with faux set to false * @param pos The positinon to start at * @return */ public String getCurrentItemId(int pos) { NetObject item = getAdapter().getItem(pos); if (item instanceof Post && ((Post)item).isNewPost()) { if (pos >= getAdapter().getCount()) { return item == null ? "" : item.getId(); } else { return getCurrentItemId(pos + 1); } } else { return item == null ? "" : item.getId(); } } /** * Adds the default padding and loading more footers in the order: * * 1. Padding * 2. Loading More */ public void addDefaultFooters() { addLoadMoreView(); } /** * Attach all linked response handlers from the helper */ public void attachHandlers() { for (String s : getResponseKeys()) { ResponseHelper.getInstance().reattach(s, this); } } /** * Detach all linked response handlers from the helper. * Only call this when the fragment is destroyed. */ public void detachHandlers() { //for (String s : getResponseKeys()) { //ResponseHelper.getInstance().detach(s); } } @Getter OnPagerListener pageListener = new OnPagerListener() { @Override public void endReached() { if (allowPagination) { if (!isDetached() && !loading && getAdapter().getStream().getHasMore() && ((MainApplication)getApplicationContext()).isConnected()) { fetchStream(getAdapter().getStream().getMinId(), true); } } } @Override public void onBreakClicked(NetObject v) { loadMissingItems(v); }; }; public CacheManager getCacheManager() { return CacheManager.getInstance(); } /** * @param p The post to append the adapter with */ public void prependItem(NetObject p) { this.adapter.addItem(0, p); this.adapter.notifyDataSetChanged(); } /** * @param p The item to delete from the adapter */ public void deleteItem(NetObject p) { this.adapter.removeItem(p.getId()); this.adapter.notifyDataSetChanged(); } /** * Creates a padding view for list views * @return The padding view */ public View createPaddingView() { Dimension dimension = new Dimension(getContext()); View padding = LayoutInflater.from(getContext()).inflate(R.layout.padding_view, null, false); padding.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, (int)(dimension.getScreenHeight() / 2.5f))); return padding; } public void addPaddingView() { this.paddingView = createPaddingView(); getHeadedListView().addFooterView(this.paddingView, null, false); } /** * Adds the loading more view into the list view */ public void addLoadMoreView() { this.loadMoreView = LayoutInflater.from(getContext()).inflate(R.layout.list_loading_footer, null); if (getAdapter() == null || (getAdapter() != null && !getAdapter().isEmpty() && getAdapter().getStream().getHasMore())) { getHeadedListView().addFooterView(this.loadMoreView, null, false); } } /** * Removes the loading more view from the list view. * NOTE: Only call this when there are no more items to load in the list */ public void removeLoadMoreView() { if (this.loadMoreView != null) { getHeadedListView().removeFooterView(this.loadMoreView); this.loadMoreView = null; } } /** * Finds a view by the resource id in mRootView * * @param id The id of the view to look for * @return The found view or null */ public View findViewById(int id) { return getView().findViewById(id); } /** * Sets the adapter of the list view * @param adapter */ public void setAdapter(RobinAdapter adapter) { this.adapter = adapter; setListAdapter(adapter); this.adapter.setOnPagerListener(pageListener); } /** * Scrolls the listview to the top */ public void scrollToTop() { runOnUiThread(new Runnable() { @Override public void run() { getListView().setSelection(0); } }); } /** * Sets the drawable of the header * @param res */ protected void setHeaderDrawable(int res) { getHeadedListView().setHeaderImage(res); } /** * Sets the drawable of the header * @param res */ protected void setHeaderUrl(String url) { getHeadedListView().setHeaderUrl(url); } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount){} /** * Called when the pull to refresh trigger is called. * * This should <b>only</b> be for user refresh */ @Override public void onRefresh() { if (((MainApplication)getApplicationContext()).isConnected()) { fetchStream("", false); showProgressLoader(); } else { getRefreshHelper().setRefreshing(false); getRefreshHelper().finish(); } } /** * Call this to implicitly refresh the stream * @param refreshView */ public void onForceRefresh() { if (((MainApplication)getApplicationContext()).isConnected()) { getRefreshHelper().setRefreshing(true); getHeadedListView().startRefresh(); if (!(((RobinSlidingActivity)getActivity()).getAdapter().getCurrentFragment() == this)) { getRefreshHelper().hideHelper(); } } else { getRefreshHelper().finish(); } } /** * This method is called in {@link #initData(Bundle)} on a fresh instantiation. * Override this and remove the <b>super</b> to stop automatic loading from API. */ public void beginLoadFromApi() { onForceRefresh(); } public void writeToCache(Stream s) { if (!TextUtils.isEmpty(getCacheFileName())) { CacheManager.getInstance().asyncWriteFile(getCacheFileName(), s, this); } } @Override public void onFinishedWriting() { finishedCacheWriting(); } public void finishedCacheWriting() { } /** * Checks the adapter sizes and removes the appropriate headers */ public void checkAdapterSizes() { Debug.out(getAdapter().getStream().getHasMore()); try { if (!getAdapter().getStream().getHasMore()) { removeLoadMoreView(); } } catch (Exception e) { Debug.out(e); } } /** * Use to fetch the stream data from cache/api * * @param lastId The last loaded Id * @param append whether to append the list or not */ public abstract void fetchStream(String lastId, final boolean append); /** * @return the cache file name associated to the page. return null if cache is not supported */ public abstract String getCacheFileName(); /** * Called at the beginning of onActivityCreated. Set up class attributes * from getArguments() Note that this method is NOT * supposed to make any change on the UI or launch whatever request. UI * changes must be made in initData() */ public abstract void retrieveArguments(Bundle arguments); /** * Called at the end of onActivityCreated or after the cache was loaded At * this point, the data should be loaded and it should do only final * treatments and UI updates */ public abstract void onDataReady(); /** * Called during onActivityCreated and onDestroy to re-attach and detach * any response handlers in the fragment. * * @return String array of the response helper keys */ public abstract String[] getResponseKeys(); /** * Called during onActivityCreated, use this to set up the list adapters * for the stream */ public abstract void setupAdapters(); /** * Override this to handle the loading of missing items in the list * @param o The item to load from */ public void loadMissingItems(NetObject o){} /** * Stub method */ @Override public void onItemClick(android.widget.AdapterView<?> arg0, View arg1, int arg2, long arg3) {}; /** * Hide the progress loader */ public void hideProgressLoader() { this.progressLoader.setVisibility(View.GONE); } /** * Show the progress loader */ public void showProgressLoader() { this.progressLoader.setVisibility(View.VISIBLE); } /** * Resets the list view to the pos position + top PX * @param pos The pos of the list * @param top The y px padding */ public void registerPositionReset(final int pos, final int top) { getListView().post(new Runnable() { @Override public void run() { getListView().setSelectionFromTop(pos, top); if (getAdapter() instanceof PostAdapter) { ((PostAdapter)getAdapter()).setAnimationsEnabled(true); } } }); } public void postRefreshAdapter() { getAdapter().notifyDataSetChanged(); getListView().post(new Runnable() { @Override public void run() { getHeadedListView().setBlockLayoutChildren(false); getHeadedListView().requestLayout(); } }); } public void refreshAdapter() { getHeadedListView().setBlockLayoutChildren(false); getAdapter().notifyDataSetChanged(); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == Constants.RESULT_REFRESH && data != null && adapter != null) { boolean refresh = false; if (data.hasExtra(Constants.EXTRA_REFRESH_TIMELINE) || data.hasExtra(Constants.EXTRA_REFRESH_ANIMATIONS) || data.hasExtra(Constants.EXTRA_REFRESH_INLINE) || data.hasExtra(Constants.EXTRA_REFRESH_NAMES) || data.hasExtra(Constants.EXTRA_REFRESH_ALL_DATA) || data.hasExtra(Constants.EXTRA_REFRESH_LIST)) { refresh = true; } if (data.hasExtra(Constants.EXTRA_REFRESH_TIMES)) { List<NetObject> items = adapter.getItems(); for (NetObject i : items) { if (i instanceof Message) { ((Message)i).setDateStr(((Message)i).calculateDateString()); } } refresh = true; } if (data.hasExtra(Constants.EXTRA_REFRESH_FONTS)) { int[] pos = getLastViewPosition(null); this.adapter.refreshFontSizes(); registerPositionReset(pos[0], pos[1]); } if (refresh) { getAdapter().notifyDataSetChanged(); } } } public class CacheLoader extends Loader<Stream> { public CacheLoader(String filename) { super(filename); } @Override public Stream doInBackground() { if (getFilename() != null) { try { Stream stream = getCacheManager().readFileAsObject(getFilename(), Stream.class); return stream; } catch (Exception e) { getCacheManager().removeFile(getFilename()); Debug.out(e); } } return null; } @Override public void onPostExecute(Stream stream) { super.onPostExecute(stream); if (getActivity() == null) { return; } if (stream != null && stream.getObjects().size() > 0) { getAdapter().setStream(stream); getAdapter().notifyDataSetChanged(); checkAdapterSizes(); } if ((stream == null || stream.getObjects().size() < 1) || (SettingsManager.getCacheTimeout() > 0 && getCacheManager().fileOlderThan(getFilename(), System.currentTimeMillis() - SettingsManager.getCacheTimeout())) || mForceRefresh) { mForceRefresh = false; // ensure we load only when the views have been created runOnUiThread(new FragmentRunnable<StreamFragment>() { @Override public void run() { getFragment().beginLoadFromApi(); } }); } stream = null; runWhenReady(new Runnable() { @Override public void run() { onDataReady(); } }); } } /********************************** * Unimplemented methods **********************************/ @Override public void onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY){} @Override public void onScrollStateChanged(AbsListView view, int scrollState){} }